/*
 * Copyright 2018 Rundeck, Inc. (http://rundeck.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dtolabs.rundeck.server.plugins.logging

import com.dtolabs.rundeck.core.execution.workflow.OutputContext
import com.dtolabs.rundeck.core.logging.LogEventControl
import com.dtolabs.rundeck.core.logging.LogLevel
import com.dtolabs.rundeck.core.logging.PluginLoggingContext
import com.dtolabs.rundeck.core.plugins.Plugin
import com.dtolabs.rundeck.core.plugins.configuration.PropertyValidator
import com.dtolabs.rundeck.core.plugins.configuration.ValidationException
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty
import com.dtolabs.rundeck.plugins.descriptions.RenderingOption
import com.dtolabs.rundeck.plugins.descriptions.RenderingOptions
import com.dtolabs.rundeck.plugins.logging.LogFilterPlugin
import com.fasterxml.jackson.databind.ObjectMapper

import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException

/**
 * @author greg
 * @since 5/17/17
 */
@Plugin(name = KeyValueDataLogFilterPlugin.PROVIDER_NAME, service = 'LogFilter')
@PluginDescription(title = 'Key Value Data',
        description = '''Captures simple Key/Value data using a regular expression.\n\n
By default, to produce a key/value entry, echo a line similar to this:

    RUNDECK:DATA:(key) = (value)

Where `(key)` is the key name, and `(value)` is the value.

If you provide a regular expression with only one group, the `name` input is required.

You can define the regular expression used.
''')

class KeyValueDataLogFilterPlugin implements LogFilterPlugin {
    public static final String PROVIDER_NAME = 'key-value-data'
    public static final String PATTERN = '^RUNDECK:DATA:\\s*([^\\s]+?)\\s*=\\s*(.+)$'
    public static final String INVALID_KEY_PATTERN = '\\s|\\$|\\{|\\}|\\\\'
    public static final String INVALID_KEY_PATTERN_DEFAULT_REPLACE_VALUE = ''
    public static final String EXTRA_SETTINGS_GROUP_NAME = "Advanced"

    @PluginProperty(
            title = "Pattern",
            description = '''Regular Expression for matching key/value data.

The regular expression must define one or two Capturing Groups. If two are defined, the first group matched defines
the data key, and the second group defines the data value. If one is defined, the capture group defines the data value,
and the **Name Data** field defines the name.

See the [Java Pattern](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html) documentation.''',
            defaultValue = KeyValueDataLogFilterPlugin.PATTERN,
            required = true,
            validatorClass = KeyValueDataLogFilterPlugin.NamePropertyValidator
    )
    String regex

    @PluginProperty(
            title = 'Name Data',
            description = '''If only one capture group is defined, the name of the captured variable'''
    )
    String name

    @PluginProperty(
            title = 'Log Data',
            description = '''If true, log the captured data''',
            defaultValue = 'false'
    )
    Boolean logData

    @PluginProperty(
            title = "Match Substrings",
            description = '''If enabled, the plugin will use Java's `find()` method instead of `matches()`
when applying the regular expression. This allows substring matches instead of requiring the entire line to match.

**Note**: Only the first match per line is captured unless "Allow Multiple Matches" is also enabled.''',
            defaultValue = "false"
    )
    Boolean matchSubstrings

    @PluginProperty(
            title = "Allow Multiple Matches",
            description = '''When "Match Substrings" is enabled, this option allows capturing multiple matches from the same line.

This option has no effect when "Match Substrings" is disabled.''',
            defaultValue = "false"
    )
    Boolean allowMultipleMatches


    @PluginProperty(
            title = "Invalid Character Pattern",
            description = '''Regular expression pattern to match invalid characters in the Key. Any matched characters will be replaced by an underscore character. Default: white space and special characters.''',
            defaultValue = KeyValueDataLogFilterPlugin.INVALID_KEY_PATTERN,
            required = false,
            validatorClass = KeyValueDataLogFilterPlugin.RegexValidator

    )
    @RenderingOptions(
            [
                    @RenderingOption(key = "groupName", value = KeyValueDataLogFilterPlugin.EXTRA_SETTINGS_GROUP_NAME),
                    @RenderingOption(key = "grouping", value = "secondary"),
                    @RenderingOption(key = "requiredValue", value = "false"),
            ]
    )
    String invalidKeyPattern

    @PluginProperty(
            title = 'Replace invalid characters',
            description = '''If checked, any invalid characters will be replaced with a defined value below''',
            defaultValue = 'false'
    )
    @RenderingOptions(
            [
                    @RenderingOption(key = "groupName", value = KeyValueDataLogFilterPlugin.EXTRA_SETTINGS_GROUP_NAME),
                    @RenderingOption(key = "grouping", value = "secondary"),
                    @RenderingOption(key = "requiredValue", value = "false"),
            ]
    )
    Boolean replaceFilteredResult

    @PluginProperty(
            title = "Replace Invalid Character Patterns With",
            description = '''If the Invalid Character Pattern matches, the string will be replaced with an underscore by default, unless you specify which value do you want to replace the invalid character pattern with.''',
            defaultValue = KeyValueDataLogFilterPlugin.INVALID_KEY_PATTERN_DEFAULT_REPLACE_VALUE,
            required = false

    )
    @RenderingOptions(
            [
                    @RenderingOption(key = "groupName", value = KeyValueDataLogFilterPlugin.EXTRA_SETTINGS_GROUP_NAME),
                    @RenderingOption(key = "grouping", value = "secondary"),
                    @RenderingOption(key = "requiredValue", value = "false"),
            ]
    )
    String invalidCharactersReplacement

    static class RegexValidator implements PropertyValidator {
        @Override
        boolean isValid(final String value) throws ValidationException {
            try {
                def compile = Pattern.compile(value)
                return true
            } catch (PatternSyntaxException e) {
                throw new ValidationException(e.message, e)
            }
        }
    }

    static class NamePropertyValidator implements PropertyValidator {

        @Override
        boolean isValid(String value) throws ValidationException {
            return false
        }

        @Override
        boolean isValid(String value, Map<String,Object> props) throws ValidationException {
            def compile

            try {
                compile = Pattern.compile(value)
            } catch (PatternSyntaxException e) {
                throw new ValidationException(e.message, e)
            }
            Matcher m = compile.matcher("");

            if(m.groupCount() == 0){
                throw new ValidationException("Pattern must have at least one group")
            }
            if(m.groupCount() == 1 && !props.containsKey("name")){
                throw new ValidationException("The Name field must be defined when only one capture group is specified")
            }
            return true
        }
    }

    Pattern dataPattern;
    OutputContext outputContext
    Map<String, String> allData
    private ObjectMapper mapper

    @Override
    void init(final PluginLoggingContext context) {
        dataPattern = Pattern.compile(regex)
        outputContext = context.getOutputContext()
        mapper = new ObjectMapper()
        allData = [:]
    }

    @Override
    void handleEvent(final PluginLoggingContext context, final LogEventControl event) {
        if (event.eventType == 'log' && event.loglevel == LogLevel.NORMAL && event.message?.length() > 0) {
            Matcher match = dataPattern.matcher(event.message)
            if (matchSubstrings && allowMultipleMatches) {
                // Find all matches in the line
                while (match.find()) {
                    processMatch(context, match)
                }
            } else {
                // Single match per line
                boolean isMatch = matchSubstrings ? match.find() : match.matches()
                if (isMatch) {
                    processMatch(context, match)
                }
            }
        }
    }

    private void processMatch(final PluginLoggingContext context, final Matcher match) {
        def key, value
        if (match.groupCount() == 1 && name) {
            key = name
            value = match.group(1)
        } else {
            key = match.group(1)
            value = match.group(2)
        }
        if (key) {
            if (invalidKeyPattern) {
                def validKey = null
                if (replaceFilteredResult) {
                    if (invalidCharactersReplacement == null) {
                        validKey = key.replaceAll(invalidKeyPattern, '')
                    } else {
                        validKey = key.replaceAll(invalidKeyPattern, invalidCharactersReplacement)
                    }
                } else {
                    validKey = key.replaceAll(invalidKeyPattern, "_")
                }
                if (key != validKey) {
                    key = validKey
                    context.log(1, "Key contains not valid value which will be replaced")
                }
            }
            allData[key] = value
            outputContext.addOutput("data", key, value)
        }
    }

    @Override
    void complete(final PluginLoggingContext context) {
        if (allData) {
            if (logData) {
                context.log(
                        2,
                        mapper.writeValueAsString(allData),
                        [
                                'content-data-type'       : 'application/json',
                                'content-meta:table-title': 'Key Value Data: Results'
                        ]
                )
            }
        }
    }
}
